
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;

import java.io.PrintStream;
import java.net.Socket;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Queue;
import java.util.LinkedList;
import java.util.List;


// this class holds the reservations database 
public class TheaterDatabase {

	int myId; // the server id
	LamportMutex lock; // the Lamport Mutex Lock object
	private Map<String, Set<Integer>> reservations; // the reservation database
	private Queue<Integer> seats; // the available seats

	public TheaterDatabase(int id) {
		lock = new LamportMutex(id);
		myId = id;
		
		this.reservations = new LinkedHashMap<String, Set<Integer>>();
		this.seats = new LinkedList<Integer>();
		for (int i = 1; i <= Constants.NUM_SEATS; i++) {
			seats.add(i);
		}
		
		boolean isSynchronized = false;
		lock.requestCS();
		int otherServerId = -1;
		while(!isSynchronized && otherServerId < Constants.NUM_SERVERS){
			otherServerId ++;
			try{
				if(myId != otherServerId){
					String dataDump = sendMsg(otherServerId, Constants.GET_DATA_DUMP);
					if(dataDump.startsWith(Constants.DATA_DUMP)){
						isSynchronized = processDataDump(dataDump);
					}
				}
			}catch (Exception e) {
				e.printStackTrace();
			}
		}
		lock.releaseCS();
		if(!isSynchronized){
			System.out.println("No other servers up. Starting with empty reservation database.");
		}else{
			System.out.println("Synchronizing data, using server# "+otherServerId);
		}
	}


	// helper method to handle reserving seats
	private String reserve(String name, int count) {
		String result = "";
		if (seats.size() < count || count <= 0) {
			result = "-1";
		} else {
			Set<String> currentNames = reservations.keySet();
			if (currentNames.contains(name)) {
				result = "-2";
			} else {
				Set<Integer> newReservations = new LinkedHashSet<Integer>();
				while (newReservations.size() < count) {
					newReservations.add(seats.poll());
				}
				reservations.put(name, newReservations);
				result = newReservations.toString();
			}
		}
		return result;
	}

	// helper method to handle searhing for a reservation by name
	private String search(String name) {
		String result = "";
		Set<Integer> assignedSeats = reservations.get(name);
		if (assignedSeats == null) {
			result = "-1";
		} else {
			result = assignedSeats.toString();
		}
		return result;
	}

	// helper method to delete a reservation
	private String delete(String name) {
		String result = "";
		Set<Integer> assignedSeats = reservations.remove(name);
		if (assignedSeats == null) {
			result = "-1";
		} else {
			result = String.valueOf(assignedSeats.size());
			seats.addAll(assignedSeats);
			Collections.sort((List<Integer>) seats);

		}
		return result;
	}

	// helper method to get movie description
	private String getInfo() {
		return Constants.MOVIE_INFO;
	}

	// helper method to handle communication from both clients and other servers
	public String process(String requestString) {
		String response = "-1";
		try {
			String[] request = requestString.split(" ");

			if (request[0].equals(Constants.LOCK_MESSAGE)) {
				// if a message is from another server relating to locking
				lock.handleMsg(requestString);
			}else if(request[0].equals(Constants.GET_DATA_DUMP)) {
					response = dataDump();
			} else {
			
				boolean synchronizeRequest = false;
				if (request[0].equals(Constants.SYNCHRONIZE)) {
					// if message is from another server pushing out changes no need to
					// lock
					// the sender still holding Lock (this is how the servers synchronize)
					for (int i = 0; i < request.length - 1; i++) {
						request[i] = request[i + 1];
					}
					synchronizeRequest = true;
				}
				if (!synchronizeRequest)// message is from client
					lock.requestCS();
				if (request[0].equals(Constants.RESERVE)) {
					response = reserve(request[1], Integer.valueOf(request[2]));
					if (!synchronizeRequest) {
						broadcastMsg(Constants.SYNCHRONIZE + " " + requestString);
					}
				} else if (request[0].equals(Constants.SEARCH)) {
					response = search(request[1]);
				} else if (request[0].equals(Constants.DELETE)) {
					response = delete(request[1]);
					if (!synchronizeRequest) {
						broadcastMsg(Constants.SYNCHRONIZE + " " + requestString);
					}
				} else if (request[0].equals(Constants.GET_INFO)) {
					response = getInfo();
				}
				if (!synchronizeRequest)
					lock.releaseCS();

			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return response;
	}

	private String dataDump() {
		String response = Constants.DATA_DUMP;
		Set<String> names = reservations.keySet();
		for (String name : names) {
			response += "\n";
			response += name + Constants.DATA_DUMP_SEPERATOR + reservations.get(name);
		}
		return response;
	}

	private boolean processDataDump(String dataDump) {
		boolean success = false;
		try{
			BufferedReader bufferedReader = new BufferedReader(new StringReader(dataDump));
			bufferedReader.readLine();
			String line = null;
			while((line = bufferedReader.readLine())!=null){
				line = line.trim();
				String name = line.substring(0, line.indexOf(Constants.DATA_DUMP_SEPERATOR));
				name.trim();
				String seatsString = line.substring(line.indexOf(Constants.DATA_DUMP_SEPERATOR)+2, line.length()-1);
				String[] reservedSeats = seatsString.split(",");
				Set<Integer> newReservations = new LinkedHashSet<Integer>();
				for(String seat:reservedSeats){
					newReservations.add(Integer.valueOf(seat.trim()));
					seats.remove(Integer.valueOf(seat.trim()));
				}
				
				reservations.put(name, newReservations);
			}
			success = true;
		}catch(Exception e){
			e.printStackTrace();
		}
		return success;
	}

	
	
	public synchronized void broadcastMsg(String message) {
		for (int i = 0; i < Constants. NUM_SERVERS; i++) {
			if (i != myId)
				sendMsg(i, message);
		}
	}

	public synchronized String sendMsg(int recipientId, String message) {
		Socket socket;
		String responseString = "";
		try {
			socket = new Socket(Constants.HOSTNAME, Constants.getPort(recipientId));
			PrintStream printStream = new PrintStream(socket.getOutputStream());
			printStream.println(message);
			printStream.flush();
			BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			responseString = bufferedReader.readLine();
			responseString = responseString.replace(Constants.CARRIAGE_RETURN, "\n");
			socket.close();
		}catch(java.net.ConnectException e){
		} catch (IOException e) {
			e.printStackTrace();
		}
		return responseString;
	}

}
